function get_re_dr         () { return ( /^DR\d(_[1-9])?$/ ) }
function get_re_tools      () { return ( /^TOOLS-/         ) }
function get_re_atp_number () { return ( /^\d\d\d\d\d?$/   ) }

function is_dr ( tag ) {
  var re = get_re_dr ();
  
  return re.test ( tag );
}

function is_tools ( tag ) {
  var re = get_re_tools ();
  
  return re.test ( tag );
}

function is_atp_number ( number ) {
  var re = get_re_atp_number ();
  
  return re.test ( number );
}

function to_option ( tag ) {
  return '<option>' + tag + '</option>';
}

function enable ( id ) {
  var node = $( id );
  
  node.disabled = '';

  if ( Prototype.Browser.IE ) {
    node.readOnly = ''; /* IE sucks */
  }
}

function disable ( id ) {
  var node = $( id );
  
  node.disabled = 'disabled';
  
  if ( Prototype.Browser.IE ) {
    node.readOnly = 'readOnly'; /* IE sucks */
  }
}

function enable_or_disable ( id, do_enable ) {
  return do_enable ? enable ( id ) : disable ( id );
}

function enable_all ( ids ) {
  ids.each ( function ( id ) { enable  ( id ) } );
}

function disable_all ( ids ) {
  ids.each ( function ( id ) { disable ( id ) } );
}

function enable_or_disable_all ( ids, do_enable ) {
  ids.each ( function ( id ) { enable_or_disable ( id, do_enable ) } );
}

function update_specification_tags ( specification, tag_id ) {
  return new Ajax.Request ( '/rest/specification/' + specification + '/tags', {
    method         : 'get',
    requestHeaders : { Accept : 'application/json' },
    onComplete     : function ( transport ) {
      var tag_n = $( tag_id );
      
      if ( specification === '-' ) {
        tag_n.update ( '<option>-</option>' );
        
        disable ( tag_id );
      }
      else {
        var dr_tags = transport.responseJSON.findAll ( is_dr );

        dr_tags ? dr_tags.unshift ( '-' ) : dr_tags = [ '-' ];

        tag_n.update ( dr_tags.collect ( function ( tag ) { return to_option ( tag ); } ) );

        enable_or_disable ( tag_id, ( dr_tags.length > 1 ) );
      }
    }
  } );
}

function update_specification_branches ( specification, branch_id ) {
  return new Ajax.Request ( '/rest/specification/' + specification + '/branches', {
    method         : 'get',
    requestHeaders : { Accept : 'application/json' },
    onComplete     : function ( transport ) {
      var branch_n = $( branch_id );
      
      if ( specification === '-' ) {
        branch_n.update ( '<option>-</option>' );
        
        disable ( branch_id );
      }
      else {
        var branches = transport.responseJSON;

        branches ? branches.unshift ( '-' ) : branches = [ '-' ];

        branch_n.update ( branches.collect ( function ( branch ) { return to_option ( branch ); } ) );

        enable_or_disable ( branch_id, ( branches.length > 1 ) );
      }
    }
  } );
}

function disable_unless ( master_id, slave_ids, enabled ) {
  return function () { enabled ( $( master_id ) ) ? enable_all ( slave_ids ) : disable_all ( slave_ids ) };
}

function link_checkboxes ( master_id, slave_ids, pre_requesites_ok ) {
  if ( pre_requesites_ok () ) {
    slave_ids.each ( function ( id ) { $( id ).checked = $( master_id ).checked } );
  }
  else {
    slave_ids.each ( function ( id ) { $( id ).checked = ''                     } );
    $( master_id ).checked = '';
  }
}

function issues_as_html_table ( issues, table_id, alone ) {
  var html =
    "<table id=\"" + table_id + "\" class=\"issues sortable\">\n"
  + "  <caption>Issues</caption>\n"
  + "  <thead>\n"
  + "    <tr>\n" + ( alone ? ""
  : "      <th class=\"noedit\" id=\"name\"                >Name</th>\n"
  + "      <th class=\"noedit\" id=\"version\"             >Version</th>\n" )
  + "      <th class=\"noedit\" id=\"issue_id\"            >ID</th>\n"
  + "      <th class=\"noedit\" id=\"requestor\"           >Requestor</th>\n"
  + "      <th class=\"noedit\" id=\"created_on\"          >Created on</th>\n"
  + "      <th class=\"noedit\" id=\"closed_on\"           >Closed on</th>\n"
  + "      <th class=\"noedit\" id=\"status\"              >Status</th>\n"
  + "      <th class=\"noedit\" id=\"owner\"               >Owner</th>\n"
  + "      <th class=\"noedit\" id=\"summary_description\" >Summary<span class=\"with_description\"> + Description</span></th>\n"
  + "    </tr>\n"
  + "  </thead>\n"
  + "  <tfoot>\n"
  + "    <tr>\n" + ( alone ? ""
  : "      <th                                             >Name</th>\n"
  + "      <th                                             >Version</th>\n" )
  + "      <th                                             >ID</th>\n"
  + "      <th                                             >Requestor</th>\n"
  + "      <th                                             >Created on</th>\n"
  + "      <th                                             >Closed on</th>\n"
  + "      <th                                             >Status</th>\n"
  + "      <th                                             >Owner</th>\n"
  + "      <th                                             >Summary<span class=\"with_description\"> + Description</span></th>\n"
  + "    </tr>\n"
  + "  </tfoot>\n"
  + "  <tbody>\n"
  ;

  var i = 1;

  issues.each ( function ( h ) {
    html = html
    + "    <tr class=\"" + ( i % 2 ? "odd" : "even" ) + "\">\n" + ( alone ? ""
    : "      <td>"                                                          + h.name            +     "</td>\n"
    + "      <td>"                                                          + h.version         +     "</td>\n" )
    + "      <td><a href=\"" + h.issue_url                          + "\">" + h.issue_id        + "</a></td>\n"
    + "      <td>"                                                          + h.requestor       +     "</td>\n"
    + "      <td>"                                                          + h.created_on      +     "</td>\n"
    + "      <td>"                                                          + h.closed_on       +     "</td>\n"
    + "      <td" + ( h.status ? " class=\"" + h.status.classify () + "\">" + h.status : ">-" ) +     "</td>\n"
    + "      <td>"                                                          + h.owner           +     "</td>\n" 
    + "      <td>\n"
    + "        <p class=\"summary\" title=\"" + h.description + "\">" + h.summary + "</p>\n"
    + "        <textarea class=\"description with_description\" readonly=\"readonly\">" + h.description + "</textarea></td>\n"
    + "      </td>\n"
    + "    </tr>\n"
    ;

    i = i + 1;
  } );

  html = html
  + "  </tbody>\n"
  + "</table>\n"
  ;
  
  return html;
}

function commits_as_html_table ( commits, table_id, n_or_id, revision, kind ) {
  var html =
    "<table id=\"" + table_id + "\" class=\"commits\">\n"
  + "  <caption>Commits</caption>\n"
  + "  <thead>\n"
  + "    <tr>\n"
  + "      <th id=\"tag\"      >Tag</th>\n"
  + "      <th id=\"revision\" >Revision</th>\n"
  + "      <th id=\"date\"     >Date</th>\n"
  + "      <th id=\"author\"   >Author</th>\n"
  + "      <th id=\"message\"  >Message</th>\n"
  + "    </tr>\n"
  + "  </thead>\n"
  + "  <tfoot>\n"
  + "    <tr>\n"
  + "      <th                 >Tag</th>\n"
  + "      <th                 >Revision</th>\n"
  + "      <th                 >Date</th>\n"
  + "      <th                 >Author</th>\n"
  + "      <th                 >Message</th>\n"
  + "    </tr>\n"
  + "  </tfoot>\n"
  + "  <tbody>\n"
  ;

  var i = 1;
  var number = ( kind == 'datasheet_id' ? 'id'  : 'number' );
  var letter = ( kind == 'datasheet_id' ? 'tag' : 'letter' );

  commits.each ( function ( h ) {
    html = html
    + "    <tr class=\"" + ( i % 2 ? "odd" : "even" ) + ( h.tag ? " tag" : "" ) + "\">\n"
    + "      <th" + ( h.tag == revision ? " class=\"current\"" : "" ) + ">" + ( h.tag ? "<a href=\"/datasheet/issues?" + number + "=" + n_or_id + "&" + letter + "=" + h.tag + "\">" + h.tag + "</a>" : '' ) + "</th>\n"
    + "      <td>"                                                          +   h.revision                                                                                                                   + "</td>\n"
    + "      <td>"                                                          +   h.date                                                                                                                       + "</td>\n"
    + "      <td>"                                                          + ( h.name || h.author )                                                                                                         + "</td>\n"
    + "      <td>\n"
    + h.message.split ( "\n" ).map ( function ( line ) {
       return "<p class=\"summary\">" + line + "</p>";
    } ).join ( "\n" )
    + "      </td>\n"
    + "    </tr>\n"
    ;

    i = i + 1;
  } );

  html = html
  + "  </tbody>\n"
  + "</table>\n"
  ;
  
  return html;
}

function products_as_html_table ( products, table_id, datasheet_id ) {
  var html =
    "<table id=\"" + table_id + "\" class=\"products sortable\">\n"
  + "  <caption>Products</caption>\n"
  + "  <thead>\n"
  + "    <tr>\n"
  + "      <th class=\"noedit\">ID</th>\n"
  + "      <th class=\"noedit\">Stage</th>\n"
  + "      <th class=\"noedit\">Datasheet Tag</th>\n"
  + "      <th class=\"noedit\">Specification ID</th>\n"
  + "      <th class=\"noedit\">Specification Branch</th>\n"
  + "    </tr>\n"
  + "  </thead>\n"
  + "  <tfoot>\n"
  + "    <tr>\n"
  + "      <th>ID</th>\n"
  + "      <th>Stage</th>\n"
  + "      <th>Datasheet Tag</th>\n"
  + "      <th>Specification ID</th>\n"
  + "      <th>Specification Branch</th>\n"
  + "    </tr>\n"
  + "  </tfoot>\n"
  + "  <tbody>\n"
  ;

  var i = 1;

  products.each ( function ( h ) {
    html = html
    + "    <tr class=\"" + ( i % 2 ? "odd" : "even" ) + "\">\n"
    + "      <td><a href=\"/specification/product?id=" + h.id +                                    "\">" +   h.id                            + "</a></td>\n"
    + "      <td>"                                                                                       + ( h.stage                || "-" ) +     "</td>\n"
    + "      <td><a href=\"/datasheet/issues?id=" + datasheet_id + "&tag=" + h.component_version + "\">" +   h.component_version             + "</a></td>\n"
    + "      <td>"                                                                                       +   h.specification_id              +     "</td>\n"
    + "      <td>"                                                                                       + ( h.specification_branch || "-" ) +     "</td>\n"
    + "    </tr>\n"
    ;

    i = i + 1;
  } );

  html = html
  + "  </tbody>\n"
  + "</table>\n"
  ;
  
  return html;
}

function display_progress ( message ) {
  if ( message != null ) {
    $( 'isock_progress_message' ).innerHTML = message;
  }
  
  $( 'isock_progress' ).style.display = 'block';
}

function hide_progress () {
  $( 'isock_progress_message' ).innerHTML = 'Please wait...';
  
  $( 'isock_progress' ).style.display = 'none';
}

function more_down ( id ) {
  $( id ).style.display = 'table';
  
  $( id + '_more_down' ).style.display = 'none';
  $( id + '_more_back' ).style.display = 'inline';
}

function more_back ( id ) {
  $( id ).style.display = 'none';
  
  $( id + '_more_down' ).style.display = 'inline';
  $( id + '_more_back' ).style.display = 'none';
}

function products_tablekit_setup () {
  TableKit.options.editAjaxURI = '/rest/product';
  
  table_kit_select_input_by_id ( '/rest/processes',      $w( 'process_id'                                        ) );
  table_kit_select_input_by_id ( '/rest/specifications', $w( 'specification_id'                                  ) );
  table_kit_select_input_by_id ( '/rest/stages',         $w( 'stage_id'                                          ) );
  table_kit_select_input_by_id ( '/rest/scm_types',      $w( 'integration_scm_type_id specification_scm_type_id' ) );
  
  table_kit_select_input_as_boolean ( $w( 'dead' ) );
}

function hide_description () {
  $$( 'table .with_description' ).each ( function ( element ) { element.style.display = 'none'   } );
}

function show_description () {
  $$( 'table .with_description' ).each ( function ( element ) { element.style.display = 'inline' } );
}

function no_description_changed () {
  if ( $( 'no_description' ).checked ) {
    hide_description ();
  }
  else {
    show_description ();
  }
}

function table_kit_select_input_by_id ( url, ids ) {
  new Ajax.Request ( url, {
    method         : 'get',
    requestHeaders : { Accept : 'application/json' },
    onComplete     : function ( transport ) {
      var couples = [ [ '-', '' ] ]; /* text, value */

      transport.responseJSON.each ( function ( couple ) { couples.push ( [ couple.id, couple.id ] ) } );

      ids.each ( function ( id ) { TableKit.Editable.selectInput ( id, {}, couples ) } );
    }
  } );
}

function table_kit_select_input_as_boolean ( ids ) {
  ids.each ( function ( id ) {
    TableKit.Editable.selectInput ( id, {}, [ [ 'yes', 'yes' ], [ 'no', 'no' ] ] );
  } );
}

function toggle_display ( klass ) {
  $$( '.' + klass ).each ( function ( node ) {
    node.style.display = ( node.style.display == 'none' ? 'table-cell' : 'none' );
  } );
  
  var node = $( klass );
  node.style.textDecoration = ( node.style.textDecoration == 'line-through' ? '' : 'line-through' );
  
  /*
  $$( '#components tbody tr' ).each ( function ( tr_n ) {
    var count     = 0;
    var undefined = 0;

    $$( '#' + tr_n.id + ' td span' ).each ( function ( span_n ) {
      count++;
      if ( span_n.className == 'undefined' ) {
        undefined++;
      }
    } );

    tr_n.style.display = ( undefined == count ? 'none' : 'table-row' );
  } );
  
  var odd_even = 'rowodd';

  $$( '#components tbody tr' ).each ( function ( tr_n ) {
    if ( tr_n.style.display != 'none' ) {
      tr_n.className = odd_even;

      odd_even = ( odd_even == 'rowodd' ? 'roweven' : 'rowodd' );
    }
    else {
      tr_n.className = '';
    }
  } );

  Tablekit.options.autoLoad = 0;
  TableKit.Sortable.init ( 'components' );
  */
}

function display_if_bad_browser ( id ) {
  $( id ).style.display = ( Prototype.Browser.IE ? 'block' : 'none' );
}

function flip_help_area ( id ) {
  $( id ).style.display = ( $( id ).style.display == 'block' ? 'none' : 'block' );
}

String.prototype.ucfirst = function () {
  return this.charAt(0).toUpperCase() + this.slice(1);
}

String.prototype.beautify = function () {
  return this.toLowerCase().ucfirst();
}

String.prototype.classify = function () {
  return this.toLowerCase().replace(" ", "_");
}

/*
** Filters a dropdown list based on a search string pattern.
**
** @param pattern : a string of zero or more characters by which to filter the list
** @param list    : reference to a form object of type 'select'
**
** (ripped from http://timothypowell.net/blog/?p=28)
*/

var dropdown_list_backup;

function filter_dropdown_list ( pattern, list ) {
  //
  // Search by regular expression, case-insensitive, from the beginning of the string.
  //
  
  pattern = '^' + pattern;
  pattern = new RegExp ( pattern, 'i' );

  //
  // If the dropdown list passed in hasn't already been backed up, do that now.
  //
  
  if ( ! dropdown_list_backup ) {
    //
    // Attach an array to the select object that is a backup of the original dropdown list.
    //
    
    var options = list.getElementsByTagName ( 'option' );
    
    dropdown_list_backup = $A( options );
  }

  //
  // Iterate through the backed up dropdown list and create a list of matches.
  //
  
  var matching_items = dropdown_list_backup.findAll ( function ( node ) { return pattern.test ( node.value ); } );

  //
  // Replace the dropdown list with the list of matches.
  //
  
  $A ( list.getElementsByTagName ( 'option' ) ).each ( function ( node ) { Element.remove ( node ); } );

  matching_items.each ( function ( node ) { Element.insert ( list, node ); } );

  //
  // Select the first item so the new options are apparent.
  //
  
  list.selectedIndex = 0;
}

function users_authorizations_table ( response, whatever, whatever_id ) {
  $$( 'select#user option' ).each ( function ( node ) { node.style.display = '' } );
  
  var html =
    "<table id=\"users_table\" class=\"users sortable editable\">\n"
  + "  <thead>\n"
  + "    <tr>\n"
  + "      <th id=\"user_id\"                class=\"noedit\">User ID</th>\n"
  + "      <th id=\"authorization_owner\"    class=\"\"      >Owner</th>\n"
  + "      <th id=\"authorization_writer\"   class=\"\"      >Writer</th>\n"
  + "      <th id=\"authorization_follower\" class=\"noedit\">Follower</th>\n"
  + "    </tr>\n"
  + "  </thead>\n"
  + "  <tfoot>\n"
  + "    <tr>\n"
  + "      <th                                               >User ID</th>\n"
  + "      <th                                               >Owner</th>\n"
  + "      <th                                               >Writer</th>\n"
  + "      <th                                               >Follower</th>\n"
  + "    </tr>\n"
  + "  </tfoot>\n"
  + "  <tbody>\n"
  ;

  var i = 1;

  response.users.each ( function ( h ) {
    if ( ! h.fake ) {
      var the_class = h.owns_all || h.writes_all ? ' class="noedit unavailable"' : '';
      var the_title = h.owns_all ? ' title="owns all"' : h.writes_all ? ' title="writes_all"' : '';

      html = html
      + "    <tr class=\"" + ( i % 2 ? "odd" : "even" ) + "\" id=\"user_id_" + h.id + "__" + whatever + "_id_" + whatever_id +"\">\n"
      + "      <td>"                              +   h.name                                       + "</td>\n"
      + "      <td" + the_class + the_title + ">" + ( h.owner    || h.owns_all    ? 'yes' : 'no' ) + "</td>\n"
      + "      <td" + the_class + the_title + ">" + ( h.writer   || h.writes_all  ? 'yes' : 'no' ) + "</td>\n"
      + "      <td class=\"unavailable\">"        + ( h.follower || h.follows_all ? 'yes' : 'no' ) + "</td>\n"
      + "    </tr>\n"
      ;

      if ( $( h.id ) ) {
        $( h.id ).style.display = 'none';
      }

      i = i + 1;
    }
  } );

  html = html
  + "  </tbody>\n"
  + "</table>\n"
  ;
  
  return html;
}

/*
** Pull/push routines for:
**
**  - specification/product (via bot/product.js)
**  - integration/product   (via bot/product.js)
**  - report/radiator
*/

function pull ( product, scope ) {
  if ( product ) {
    new Ajax.Request ( '/rest/product/' + product + '/pull?scope=' + scope + '&dummy=1', {
      method         : 'put',
      requestHeaders : { Accept : 'application/json' },
      onCreate       : function () {
        display_progress ( 'Querying ' + scope + '...' );
      },
      onSuccess      : function ( response ) {
        var s = '';
        response.responseJSON.actions.each ( function ( h ) { s = s + "- " + h.description + "\n"; } );

        if ( s ) {
          show_popup ( null, s, product, scope, response.responseJSON.data );
        }
        else {
          alert ( 'No change on ' + product + ' product.' );
        }
      },
      onFailure      : function ( response ) {
        display_error ( response.responseJSON.error );
      },
      onComplete     : function () {
        hide_progress ();
      }
    } );
  }
}

function push ( product, scope, data ) {
  new Ajax.Request ( '/rest/product/' + product + '/push', {
    method         : 'post',
    parameters     : { 'scope' : scope, 'data' : Object.toJSON ( data ) }, // instead of postBody : data (i.e., mutex with parameters)
    requestHeaders : { Accept : 'application/json' },
    onCreate       : function () {
      display_progress ( 'Pushing ' + scope + '...' );
    },
    onSuccess      : function ( response ) {
      alert ( 'Done.' );
      
      hide_popup ();
    },
    onFailure      : function ( response ) {
      display_error ( response.responseJSON.error );
    },
    onComplete     : function () {
      hide_progress ();
    }
  } );
}

function show_popup ( event, content, product, scope, data, download ) {
  var buttons =
    '<div class="together">'
  +   '<input id="hide_ci_log" type="button" value="Close"/>'
  + ( product ?
      '<input id="push"     type="button" value="Push"/>'     : '' )
  + ( download ?
      '<a id="download" href="' + download + '">Download</a>' : '' )
  + '</div>'
  ;
  
  $( 'popup' ).replace ( '<div id="popup">' + buttons + '<textarea readonly="readonly">' + content + '</textarea></div>' );
  $( 'popup' ).style.display = 'block';
/*if ( event ) {
    $( 'popup' ).appear ({ duration : 10 });
  }*/

  Event.observe ( 'hide_ci_log', 'click', hide_popup );
  
  if ( product ) {
    Event.observe ( 'push', 'click', function () { push ( product, scope, data ); } );
  }
}

function hide_popup ( event ) {
/*$( 'popup' ).fade ({ duration : 10 });*/
  $( 'popup' ).replace ( '<div id="popup" />' );
  $( 'popup' ).style.display = 'none';
}

function display_error ( error ) {
  alert ( 'Failed: \u00AB ' + error + " \u00BB" ); /* &laquo; "Left-pointing double angle quotation mark" + &raquo; "Right-pointing double angle quotation mark" */
}

/*
** Requires:
**
**   - s2.js
**   - ScaleColor.js
**   - ProtoCloud.js
** 
** ProtoCloud:
**
**   - https://github.com/tfluehr/ProtoCloud
*/

function fill_the_hits ( type, template, show_tool_tip ) {
  if ( type != '' ) {
    new Ajax.Request ( '/rest/hits', {
      method         : 'get',
      requestHeaders : { Accept : 'application/json' },
      onFailure      : function () {
        $( 'hits' ).replace ( '<div id="hits"/>' );
      },
      onComplete     : function ( transport ) {
        new ProtoCloud ( 'hits', {
          data           : transport.responseJSON.select ( function ( h ) { return h.type == type } ),
          baseColor      : '#002A8B',
          showTooltip    : show_tool_tip,
          hrefTemplate   : template
        } );
      }
    } );
  }
  else {
    $( 'hits' ).replace ( '<div id="hits"/>' );
  }
}

/*
** Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license
** Author: Jim Palmer (based on chunking idea from Dave Koelle)
**
** http://www.overset.com/2008/09/01/javascript-natural-sort-algorithm/
*/

function natural_sort (a, b) {
    var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,
        sre = /(^[ ]*|[ ]*$)/g,
        dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
        hre = /^0x[0-9a-f]+$/i,
        ore = /^0/,
        i = function(s) { return naturalSort.insensitive && (''+s).toLowerCase() || ''+s },
        // convert all to strings strip whitespace
        x = i(a).replace(sre, '') || '',
        y = i(b).replace(sre, '') || '',
        // chunk/tokenize
        xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        // numeric, hex or date detection
        xD = parseInt(x.match(hre)) || (xN.length != 1 && x.match(dre) && Date.parse(x)),
        yD = parseInt(y.match(hre)) || xD && y.match(dre) && Date.parse(y) || null,
        oFxNcL, oFyNcL;
    // first try and sort Hex codes or Dates
    if (yD)
        if ( xD < yD ) return -1;
        else if ( xD > yD ) return 1;
    // natural sorting through split numeric strings and default strings
    for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) {
        // find floats not starting with '0', string or 0 if not defined (Clint Priest)
        oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0;
        oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0;
        // handle numeric vs string comparison - number < string - (Kyle Adams)
        if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; }
        // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
        else if (typeof oFxNcL !== typeof oFyNcL) {
            oFxNcL += '';
            oFyNcL += '';
        }
        if (oFxNcL < oFyNcL) return -1;
        if (oFxNcL > oFyNcL) return 1;
    }
    return 0;
}
